﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Diagnostics;

namespace Xceed.Silverlight.Data
{
  internal sealed class DataRange
  {
    private DataRange(
      IDictionary<string, object> rangeStart,
      SourcePosition rangeStartPosition,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      SourcePosition rangeEndPosition,
      bool endInclusive,
      DataRangeContext context,
      bool isSelectionRangeInclusive )
    {
      if( rangeStartPosition == null )
        throw new ArgumentNullException( "rangeStartPosition" );

      if( rangeEndPosition == null )
        throw new ArgumentNullException( "rangeEndPosition" );

      if( rangeStart == null && !rangeStartPosition.IsOutOfBound )
        throw new ArgumentException( "rangeStart cannot be null when the rangeStartPosition is within bounds.", "rangeStart" );

      if( rangeEnd == null && !rangeEndPosition.IsOutOfBound )
        throw new ArgumentException( "rangeEnd cannot be null when the rangeEndPosition is within bounds.", "rangeEnd" );

      if( context == null )
        throw new ArgumentNullException( "context" );

      m_startRange = rangeStart;
      m_startRangePosition = rangeStartPosition;
      m_isStartInclusive = startInclusive;
      m_endRange = rangeEnd;
      m_endRangePosition = rangeEndPosition;
      m_isEndInclusive = endInclusive;
      m_context = context;
      m_isSelectionRangeInclusive = isSelectionRangeInclusive;

      this.CheckDataRangeConsistency();
      this.MakeStartRangeLowest();
    }

    #region StartPosition Property (Internal)

    internal SourcePosition StartPosition
    {
      get
      {
        return m_startRangePosition;
      }
    }

    private SourcePosition m_startRangePosition; //null

    #endregion StartPosition Property (Internal)

    #region EndPosition Property (Internal)

    internal SourcePosition EndPosition
    {
      get
      {
        return m_endRangePosition;
      }
    }

    private SourcePosition m_endRangePosition; //null

    #endregion EndPosition Property (Internal)

    #region IsStartPositionInclusive Property (Internal)

    internal bool IsStartPositionInclusive
    {
      get
      {
        return m_isStartInclusive;
      }
    }

    private bool m_isStartInclusive; //false

    #endregion IsStartPositionInclusive Property (Internal)

    #region IsEndPositionInclusive Property (Internal)

    internal bool IsEndPositionInclusive
    {
      get
      {
        return m_isEndInclusive;
      }
    }

    private bool m_isEndInclusive; //false

    #endregion IsEndPositionInclusive Property (Internal)

    #region Context Property (Internal)

    internal DataRangeContext Context
    {
      get
      {
        return m_context;
      }
    }

    private DataRangeContext m_context; //null

    #endregion Context Property (Internal)

    #region IsSelectionRangeInclusive Property (Internal)

    internal bool IsSelectionRangeInclusive
    {
      get
      {
        return m_isSelectionRangeInclusive;
      }
    }

    private bool m_isSelectionRangeInclusive; //false

    #endregion IsSelectionRangeInclusive Property (Internal)

    private static IDictionary<string, object> ExtractValues(
      DataPath dataPath,
      IEnumerable<SortDescription> sortDescriptions )
    {
      Dictionary<string, object> keyValues = new Dictionary<string, object>();
      RawDataItem dataItem = ( RawDataItem )dataPath.LastChild;

      foreach( SortDescription sortDescription in sortDescriptions )
      {
        string memberName = sortDescription.PropertyName;
        object value = dataItem.GetDataMemberValue( memberName );

        keyValues.Add( memberName, value );
      }

      return keyValues;
    }

    private static SortPosition CreateSortPosition(
      IDictionary<string, object> keyValues,
      IList<SortDescription> sortDescriptions,
      bool startBefore )
    {
      if( keyValues == null || sortDescriptions == null )
        return null;

      IList<object> values = new List<object>();

      foreach( SortDescription sort in sortDescriptions )
      {
        object value;

        if( keyValues.TryGetValue( sort.PropertyName, out value ) )
        {
          values.Add( value );
        }        
      }

      StartPosition startPosition = ( startBefore )
                                      ? Xceed.Silverlight.Data.StartPosition.Before
                                      : Xceed.Silverlight.Data.StartPosition.After;

      return new SortPosition(
                   sortDescriptions,
                   values,
                   startPosition );
    }

    private static ItemPosition CreateItemPosition( DataPath dataPath )
    {
      if( dataPath == null )
        return null;

      RawDataItem dataItem = dataPath.LastChild as RawDataItem;

      if( dataItem == null )
        return null;

      return new ItemPosition( dataPath );
    }

    private static Func<string, object> GetMemberValue( RawDataItem dataItem )
    {
      return dataItem.GetDataMemberValue;
    }

    private static int CompareValueHelper( object a, object b )
    {
      if( a == b )
        return 0;

      if( a == null )
        return -1;

      if( b == null )
        return 1;

      IComparable comparable = a as IComparable;

      if( comparable == null )
        throw new ArgumentException( "The specified objects must implement IComparable." );

      return comparable.CompareTo( b );
    }

    private static int CompareHelper(
      RawDataItem dataItem,
      IDictionary<string, object> keyValues,
      bool isRangeInclusive,
      IEnumerable<SortDescription> sortDescriptions )
    {
      int compareResult = 0;

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue;
        object secondValue;
        bool isFirstValueFound = dataItem.TryGetDataMemberValue( propName, out firstValue );
        bool isSecondValueFound = keyValues.TryGetValue( propName, out secondValue );

        compareResult = DataRange.CompareValueHelper( isFirstValueFound, firstValue, true, isSecondValueFound, secondValue, isRangeInclusive, sortDesc );
        if( compareResult != 0 )
          return compareResult;  
      }

      return 0;
    }

    internal static int CompareHelper(
      IDictionary<string, object> first,
      bool isFirstRangeInclusive,
      IDictionary<string, object> second,
      bool isSecondRangeInclusive,
      IEnumerable<SortDescription> sortDescriptions )
    {
      int compareResult = 0;

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue;
        object secondValue;
        bool isFirstValueFound = first.TryGetValue( propName, out firstValue );
        bool isSecondValueFound = second.TryGetValue( propName, out secondValue ); 

        compareResult = DataRange.CompareValueHelper( isFirstValueFound, firstValue, isFirstRangeInclusive, isSecondValueFound, secondValue, isSecondRangeInclusive, sortDesc );
        if( compareResult != 0 )
          return compareResult;       
      }

      return 0;
    }

    private static int CompareValueHelper( bool isFirstValueFound, object firstValue, bool isFirstRangeInclusive, 
                                          bool isSecondValueFound, object secondValue,  bool isSecondRangeInclusive, SortDescription sortDesc )
    {
      if( isFirstValueFound && isSecondValueFound )
      {
        int compare = DataRange.CompareValueHelper( firstValue, secondValue );
        return ( sortDesc.Direction == ListSortDirection.Ascending ) ? compare : -compare;
      }
      else if( isFirstValueFound )
      {
        return ( sortDesc.Direction == ListSortDirection.Ascending ) ? 1 : -1;
      }
      else if( isSecondValueFound )
      {
        return ( sortDesc.Direction == ListSortDirection.Ascending ) ? -1 : 1;
      }

      return 0;
    }

    private ListSortDirection GetSortAffectingDataRangeDirection()
    {
      foreach( SortDescription sortDesc in m_context.SortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue = null;
        object secondValue = null;
        bool isFirstValueFound = false;
        bool isSecondValueFound = false;

        if( m_startRange != null )
        {
          isFirstValueFound = m_startRange.TryGetValue( propName, out firstValue ); 
        }
        if( m_endRange != null )
        {
          isSecondValueFound = m_endRange.TryGetValue( propName, out secondValue ); 
        }

        bool isSameValues = false;

        if( isFirstValueFound && isSecondValueFound )
          isSameValues = ( firstValue == secondValue );
        else if( !isFirstValueFound && !isSecondValueFound )
          isSameValues = true;

        if( !isSameValues )
          return sortDesc.Direction;
      }

      return ListSortDirection.Ascending;
    }

    private void SwapBoundaries()
    {
      IDictionary<string, object> swapKeyValues = m_endRange;
      SourcePosition swapSourcePosition = m_endRangePosition;
      bool swapInclusive = m_isEndInclusive;

      m_endRange = m_startRange;
      m_endRangePosition = m_startRangePosition;
      m_isEndInclusive = m_isStartInclusive;

      m_startRange = swapKeyValues;
      m_startRangePosition = swapSourcePosition;
      m_isStartInclusive = swapInclusive;

      // The StartPosition flag within the SortPosition
      // must be inversed to keep the same range.
      if( m_startRangePosition is SortPosition )
      {
        m_startRangePosition = DataRange.CreateSortPosition(
                                 m_startRange,
                                 m_context.SortDescriptions,
                                 m_isStartInclusive );
      }

      if( m_endRangePosition is SortPosition )
      {
        m_endRangePosition = DataRange.CreateSortPosition(
                               m_endRange,
                               m_context.SortDescriptions,
                               !m_isEndInclusive );
      }
    }

    private void CheckDataRangeConsistency()
    {
      ValidateRangeInfoSorts( m_startRange );
      ValidateRangeInfoSorts( m_endRange );

      ValidateRangeInfoPreviousSorts( m_startRange );
      ValidateRangeInfoPreviousSorts( m_endRange );      
    }

    private void ValidateRangeInfoSorts( IDictionary<string, object> rangeInfo )
    {
      IList<SortDescription> sorts = m_context.SortDescriptions;

      // Check if all sort criteria of the StartRangeInfo/EndRangeInfo are contained in the Range's sort descriptions
      if( rangeInfo != null )
      {
        for( int i = 0; i < rangeInfo.Count; ++i )
        {
          bool isKeyInRange = sorts.Any( ( SortDescription ) => SortDescription.PropertyName == rangeInfo.ElementAt( i ).Key );
          if( !isKeyInRange )
          {
            throw new ArgumentException( "The specified range info contains the value '" + rangeInfo.ElementAt( i ).Key + "', which is not a valid range sort.", "rangeInfo" );
          }
        }
      }
    }

    private void ValidateRangeInfoPreviousSorts( IDictionary<string, object> rangeInfo )
    {
      IList<SortDescription> sorts = m_context.SortDescriptions;

      // Check if a Range sort is in the StartRangeInfo/EndRangeInfo, if yes, make sure all previous range sorts are included.
      if( rangeInfo != null )
      {
        object outValue = null;

        for( int i = sorts.Count - 1; i >= 0; --i )
        {
          if( rangeInfo.TryGetValue( sorts[ i ].PropertyName, out outValue ) )
          {
            for( int j = 0; j <= i - 1; ++j )
            {
              if( !rangeInfo.TryGetValue( sorts[ j ].PropertyName, out outValue ) )
              {
                throw new ArgumentException( "The specified range info is missing missing the sort '" + sorts[ j ].PropertyName + "' primary sort.", "rangeInfo" );
              }
            }
            return;
          }
        }
      }
    }

    internal bool IsSameRange( DataRange obj )
    {
      return ( DataRange.AreSamePositions( this.StartPosition, obj.StartPosition ) )
          && ( DataRange.AreSamePositions( this.EndPosition, obj.EndPosition ) )
          && ( object.Equals( this.Context, obj.Context ) );
    }

    private static bool AreSamePositions( SourcePosition firstPosition, SourcePosition secondPosition )
    {
      bool areSame = ( firstPosition == secondPosition );

      if( areSame )
        return areSame;

      ItemPosition firstItemPosition = firstPosition as ItemPosition;
      ItemPosition secondItemPosition = secondPosition as ItemPosition;

      if( ( firstItemPosition != null ) || ( secondItemPosition != null ) )
      {
        if( ( firstItemPosition == null ) || ( secondItemPosition == null ) )
          return false;

        return object.Equals( firstItemPosition.Item, secondItemPosition.Item );
      }

      SortPosition firstSortPosition = firstPosition as SortPosition;
      SortPosition secondSortPosition = secondPosition as SortPosition;

      if( ( firstSortPosition != null ) || ( secondSortPosition != null ) )
      {
        if( ( firstSortPosition == null ) || ( secondSortPosition == null ) )
          return false;

        return ( firstSortPosition.StartPosition == secondSortPosition.StartPosition )
            && ( firstSortPosition.SortingReference.SequenceEqual( secondSortPosition.SortingReference ) )
            && ( firstSortPosition.SortValues.SequenceEqual( secondSortPosition.SortValues ) );
      }

      return areSame;
    }

    private void MakeStartRangeLowest()
    {
      // Make sure the range start is lower or equal than the range end.
      bool isStartRangeOutOfBound = m_startRangePosition.IsOutOfBound;
      bool isEndRangeOutOfBound = m_endRangePosition.IsOutOfBound;
      IList<SortDescription> sorts = m_context.SortDescriptions;

      if( !isStartRangeOutOfBound && !isEndRangeOutOfBound )
      {
        int compare = DataRange.CompareHelper( m_startRange, m_isStartInclusive, m_endRange, m_isEndInclusive, sorts );
        if( compare > 0 )
        {
          this.SwapBoundaries();
        }
      }
      else
      {
        if( isStartRangeOutOfBound )
        {
          Debug.Assert( m_startRangePosition.IsBeforeFirst );
        }
        if( isEndRangeOutOfBound )
        {
          Debug.Assert( m_endRangePosition.IsAfterLast );
        }
      }
    }

    internal bool Contains( DataPath item )
    {
      // A DataPath is contained in the range only if it is within the boundaries
      // and it isn't filtered out by a local or remote filter.
      return !this.IsOutside( item )
          && !this.IsFilteredOut( item );
    }

    internal bool IsOutside( DataPath item )
    {
      bool isOutside = true;

      // Only user objects are allowed in the range.
      if( item != null && item.LastChild is RawDataItem )
      {
        isOutside = false;
        RawDataItem dataItem = ( RawDataItem )item.LastChild;
        IEnumerable<SortDescription> sorts = m_context.SortDescriptions;

        if( !isOutside && m_startRange != null )
        {
          int compare = DataRange.CompareHelper( dataItem, m_startRange, m_isStartInclusive, sorts );

          isOutside = ( compare < 0 || ( compare == 0 && !m_isStartInclusive ) );
        }

        if( !isOutside && m_endRange != null )
        {
          int compare = DataRange.CompareHelper( dataItem, m_endRange, m_isEndInclusive, sorts );

          isOutside = ( compare > 0 || ( compare == 0 && !m_isEndInclusive ) );
        }
      }

      return isOutside;
    }

    internal bool IsFilteredOut( DataPath item )
    {
      bool isFilteredOut = true;

      // Only user objects are allowed in the range.
      if( item != null && item.LastChild is RawDataItem )
      {
        isFilteredOut = false;
        RawDataItem dataItem = ( RawDataItem )item.LastChild;

        if( m_filterPredicate == null )
        {
          Predicate<object> localFilter = m_context.Filter;
          Func<object, bool> remoteFilter = null;

          if( m_context.FilterExpression != null )
          {
            PredicateBuilder pb = new PredicateBuilder( m_context.FilterExpression );

            remoteFilter = pb.ToPredicate<object>( m_context.UserObjectType, null );
          }

          if( localFilter != null || remoteFilter != null )
          {
            m_filterPredicate =
              ( object value ) =>
              {
                if( localFilter != null )
                {
                  if( !localFilter.Invoke( ( ( RawDataItem )value ).Data ) )
                    return false;
                }

                if( remoteFilter != null )
                {
                  if( !remoteFilter.Invoke( value ) )
                    return false;
                }

                return true;
              };
          }
        }

        if( m_filterPredicate != null )
        {
          isFilteredOut = !m_filterPredicate.Invoke( dataItem );
        }
      }

      return isFilteredOut;
    }

    internal static DataRange All( bool isSelectionRangeInclusive, DataRangeContext context )
    {
      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From( 
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart, 
      bool startInclusive, 
      DataRangeContext context )
    {
      SortPosition position = null;

      if( context != null )
      {
        position = DataRange.CreateSortPosition(
                     rangeStart,
                     context.SortDescriptions,
                     startInclusive );
      }

      return new DataRange(
                   rangeStart,
                   position,
                   startInclusive,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition position = DataRange.CreateItemPosition( rangeStart );

      if( position != null && context != null )
      {
        keyValues = DataRange.ExtractValues( rangeStart, context.SortDescriptions );
      }

      return new DataRange(
                   keyValues,
                   position,
                   startInclusive,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      SortPosition rangeStartPosition = null;
      SortPosition rangeEndPosition = null;

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        rangeStartPosition = DataRange.CreateSortPosition(
                               rangeStart,
                               sorts,
                               startInclusive );
        rangeEndPosition = DataRange.CreateSortPosition(
                             rangeEnd,
                             sorts,
                             !endInclusive );
      }

      return new DataRange(
                   rangeStart,
                   rangeStartPosition,
                   startInclusive,
                   rangeEnd,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition rangeStartPosition = DataRange.CreateItemPosition( rangeStart );
      SortPosition rangeEndPosition = null;

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        if( rangeStartPosition != null )
        {
          keyValues = DataRange.ExtractValues( rangeStart, context.SortDescriptions );
        }

        rangeEndPosition = DataRange.CreateSortPosition(
                             rangeEnd,
                             sorts,
                             !endInclusive );
      }

      return new DataRange(
                   keyValues,
                   rangeStartPosition,
                   startInclusive,
                   rangeEnd,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      SortPosition rangeStartPosition = null;
      ItemPosition rangeEndPosition = DataRange.CreateItemPosition( rangeEnd );

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        rangeStartPosition = DataRange.CreateSortPosition(
                               rangeStart,
                               sorts,
                               startInclusive );

        if( rangeEndPosition != null )
        {
          keyValues = DataRange.ExtractValues( rangeEnd, sorts );
        }
      }

      return new DataRange(
                   rangeStart,
                   rangeStartPosition,
                   startInclusive,
                   keyValues,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      bool isTargetingSingleItem = ( rangeStart == rangeEnd );
      IDictionary<string, object> rangeStartValues = null;
      IDictionary<string, object> rangeEndValues = null;
      ItemPosition rangeStartPosition = DataRange.CreateItemPosition( rangeStart );
      ItemPosition rangeEndPosition;

      if( isTargetingSingleItem )
      {
        rangeEndPosition = rangeStartPosition;
      }
      else
      {
        rangeEndPosition = DataRange.CreateItemPosition( rangeEnd );
      }

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        if( rangeStartPosition != null )
        {
          rangeStartValues = DataRange.ExtractValues( rangeStart, sorts );
        }

        if( rangeEndPosition != null )
        {
          if( isTargetingSingleItem )
          {
            rangeEndValues = rangeStartValues;
          }
          else
          {
            rangeEndValues = DataRange.ExtractValues( rangeEnd, sorts );
          }
        }
      }

      return new DataRange(
                   rangeStartValues,
                   rangeStartPosition,
                   startInclusive,
                   rangeEndValues,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange To(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      SortPosition position = null;

      if( context != null )
      {
        position = DataRange.CreateSortPosition(
                     rangeEnd,
                     context.SortDescriptions,
                     !endInclusive );
      }

      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   rangeEnd,
                   position,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange To(
      bool isSelectionRangeInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition position = DataRange.CreateItemPosition( rangeEnd );

      if( position != null && context != null )
      {
        keyValues = DataRange.ExtractValues( rangeEnd, context.SortDescriptions );
      }

      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   keyValues,
                   position,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }


    #region Private Fields

    private IDictionary<string, object> m_startRange; //null
    private IDictionary<string, object> m_endRange; //null
    private Func<object, bool> m_filterPredicate; //null

    #endregion Private Fields
  }
}
